/* memo.c

   An ultra-simple recorded audio tool
   Written by Daniel J. Shields <d4nshields@gmail.com>
   Depends on SDL_audioin by Tyler Montbriand
   The method write_wav_headers() was derived from a work Copyright (C) 2002 Jean-Marc Valin, used within license guidelines.
   Created: January 1, 2009.
   Last modified: March 9, 2009.
*/
#define DEBUG 1		// 1 during development 0 at other times

#include <stdio.h>
#include <string.h>  // For "strdup()"
#include <libintl.h>  // For "gettext()"

#ifdef WIN32
#elif __BEOS__
#elif __APPLE__
#else
# include "sys/stat.h" // for mkdir()
# include "sys/time.h" // for gettimeofday()
#endif

#include <playsound.h>
#include "tp_magic_api.h"  // Tux Paint "Magic" tool API header
#include "SDL_image.h"  // For IMG_Load(), to load our PNG icon

#ifndef NOSOUND
# include "SDL_mixer.h"  // For Mix_LoadWAV(), to load our sound effects
#endif

#define AUDIO_SAMPLERATE		44100
#define AUDIO_SAMPLES			1000
#define AUDIO_CHANNELS			1
#define AUDIO_FORMAT			AUDIO_S16
#define CHUNK_SIZE			512
#define AUDIO_SAMPLESIZE	sizeof( signed short)

#define MEMO_CHAN	-1

// I don't know the exact values that should go here for each platform.  PATH_MAX
// is the maximum number of characters allowed in a fully qualified path name. (PLEASE FILL IN BELOW)
#ifdef WIN32
# define PATH_MAX	???
#elif __BEOS__
# define PATH_MAX	???
#elif __APPLE__
# define PATH_MAX	???
#else
# define PATH_MAX	4096
#endif

#define MAX_PLAYQUEUE	8		// number of lookahead entries in our playback_QueueSound circular buffer

enum
{
	TOOL_MEMO_RECORD,
	NUM_TOOLS
};

typedef struct {
	const char *icon_filename;
	const char *snd_filename_start;
	const char *snd_filename_stop;
	const char *name;
	const char *desc;
} toolsstru;

toolsstru tools[NUM_TOOLS] = 
{
	{
		"memo-record.png",
		"memo-record-start.ogg",
		"memo-record-stop.ogg",
		gettext_noop ( "Record"),
		gettext_noop ( "Add recorded audio to your picture."),
	}
};

static int audioin_initialised = 0;
static Mix_Chunk * snd_effect_start[NUM_TOOLS];
static Mix_Chunk * snd_effect_stop[NUM_TOOLS];
static int playback_isplaying = 0;		// 1 (true) if there are WAVs being played
static int playback_queueHead = 0;		// location currently playback_isplaying back
static int playback_queueTail = 0;		// keeps track of the largest location in our queue used
static Mix_Chunk *playback_playQueue[ MAX_PLAYQUEUE];
static FILE *recordto_filep = NULL;
static char audio_out_filename[ PATH_MAX];

static char * getSaveDir();
static void write_wav_header(FILE *file, int rate, int channels, int format, int size);
static void game_SilenceDetected();
static void game_StartCapture();
static void game_Stop();

/* playback_Next is a chained callback to play consecutively queued Mix_Chunks without using a thread
/* --------------------- */
static void playback_Next( int channel)
{
	if( playback_queueHead < playback_queueTail)
	{
		playback_isplaying = 1;
		Mix_SetPanning( channel, 255, 255);
		Mix_ChannelFinished( playback_Next);		// callback to self when Mix_Chunk is finished
		Mix_PlayChannel( channel, playback_playQueue[playback_queueHead++ % MAX_PLAYQUEUE], 0);
	} else {
		Mix_ChannelFinished(NULL);
		playback_isplaying = 0;
	}
	return;
}

static void playback_Abort()
{
	playback_queueHead = playback_queueTail;
	Mix_ChannelFinished(NULL);
	Mix_HaltChannel(MEMO_CHAN);
	playback_isplaying = 0;
}

// playback_QueueSound fills the queue at the tail but does NOT allow the circular queue to overflow;  
// this method will block until there is a free entry, when full.
static void playback_QueueSound( int channel, Mix_Chunk * mixChunk)
{
	while( playback_queueTail - playback_queueHead >= MAX_PLAYQUEUE)
		SDL_Delay(10);
	playback_playQueue[ playback_queueTail++ % MAX_PLAYQUEUE] = mixChunk;

	if( !playback_isplaying)
	{
		playback_Next( channel);
	}
	return;
}


/* Choose a filename based on the localtime */
static void get_new_file_id(void)
{
	time_t t;
	
	t = time(NULL);
	
	strftime(audio_out_filename, sizeof(audio_out_filename), "%Y%m%d%H%M%S.wav", localtime(&t));
}

#define ABS(x) (x<0 ? -x: x)
#define SILENCE_THRESHOLD 1000
#define SECS_SILENCE_START 10		// # of silent secs that may be trimmed from the start of the sample
#define SECS_SILENCE_END 2		// # of silent secs at the end of the sample that indicate the end
static long capture_numSilentSamples = 0L;
static int isLeadingSilence = 1;

/* --------------------- */
static int capture_Start() 
{
	char output_path[PATH_MAX];
	FILE * capturedest;
#ifdef DEBUG
	fprintf( stderr, "capture_Start()\n");
#endif
	get_new_file_id();
	
	snprintf ( output_path, sizeof ( output_path),
		"%s/memos",
		getSaveDir());
	mkdir( output_path, S_IXUSR | S_IWUSR | S_IRUSR );
#ifdef DEBUG
	fprintf( stderr, "capture_Start( audio_out_filename = %s)\n", audio_out_filename);
#endif
	strncat( output_path, "/", sizeof( output_path));
	strncat( output_path, audio_out_filename, sizeof( output_path));

	if( !(capturedest=fopen( output_path, "wb")) ) {
		fprintf( stderr, "couldn't open output file: %s\n", output_path);
		perror( "memo:capture_Start()");
		return -1;
	}
	write_wav_header( capturedest, AUDIO_SAMPLERATE, AUDIO_CHANNELS, AUDIO_FORMAT, 0);

	// intialise the state vars of capture_ProcessAudioData() callback
	capture_numSilentSamples = 0L;
	isLeadingSilence = 1;
	recordto_filep = capturedest;		// setting this pointer connects the audioin callback to the dest FILE *
	return 0;
}

/* --------------------- */
static int capture_Stop()
{
  FILE * capturedest;

  if( recordto_filep) {
#ifdef DEBUG
	fprintf( stderr, "capture_Stop()\n");
#endif

	capturedest = recordto_filep;
	recordto_filep=NULL;		// stop the capturing in the callback (which might still be happening)
		
	long captureLength = ftell( capturedest); // get length of size of the file to compute WAV header
	fseek( capturedest, (long)0, SEEK_SET); // go back to beginning of file to write the header again
	write_wav_header( capturedest, AUDIO_SAMPLERATE, AUDIO_CHANNELS, AUDIO_FORMAT, (int)captureLength);
	fclose( capturedest);
  }
  return 0;
}

static long unsigned usec_max = 0L;

/* --------------------- */
static void capture_ProcessAudioData(void *userdata, Uint8 *stream, int len)
{
	// silence detection analysis here
	// how many seconds of silence have occurred?
	char fname[PATH_MAX];
        int i=0;
        Sint16 *sData=(Sint16 *)stream;
        int samples=(len/AUDIO_SAMPLESIZE)-1;
	int isSilent = 1;
//	struct timeval tvp;

//	gettimeofday( &tvp, NULL); fprintf( stderr, "A: us = %ld\n", tvp.tv_usec);
//	long unsigned usec_start = tvp.tv_usec;
//	long unsigned usec_end = 0L;

	for( i=0; i < samples; i++) {
		if( ABS( sData[i]) > SILENCE_THRESHOLD) {
			isSilent = 0;
			break;
		}
	}
	if( isSilent) {
		capture_numSilentSamples += (long)samples;
	} else {
		capture_numSilentSamples = 0L;
		isLeadingSilence = 0;
	}

	// trim up to SECS_SILENCE_START silent seconds from the start of the sample, and allow 
	// up to SECS_SILENCE_END silent seconds at the end of the sample before triggering
	// the game_SilenceDetected() event.
	long secs_silence_highwatermark = ( isLeadingSilence ? SECS_SILENCE_START : SECS_SILENCE_END);

//	gettimeofday( &tvp, NULL); fprintf( stderr, "B: us = %ld\n", tvp.tv_usec);

	if( recordto_filep) {
		if( capture_numSilentSamples > AUDIO_SAMPLERATE * secs_silence_highwatermark) {
			game_SilenceDetected();
			capture_numSilentSamples = 0;
			isLeadingSilence = 1;
		}
	}
	if( recordto_filep) {

//		gettimeofday( &tvp, NULL); fprintf( stderr, "C: us = %ld\n", tvp.tv_usec);

#if DEBUG
//		fprintf( stderr, "RECORDING\n");
//		if( isLeadingSilence)
//			fprintf( stderr, "LEADING SILENCE\n");
#endif
		if( !isLeadingSilence) {
			if( fwrite( stream, sizeof( Uint8), len, recordto_filep) < len) {
				perror( "capture_ProcessAudioData");
			}
		}

//		gettimeofday( &tvp, NULL); fprintf( stderr, "D: us = %ld\n", tvp.tv_usec);

	}
//	usec_end = tvp.tv_usec;
//	usec_max = ( (usec_end - usec_start) > usec_max ? usec_end - usec_start : usec_max);
//	fprintf( stderr, "%ld/%ld useconds spent in callback\n", usec_end - usec_start, usec_max);
}

/* --------------------- */
/* little endian routines */
static inline Uint32 littleendian_32(Uint32 i)
{
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
   return i>>24 | ((i>>8)&0x0000ff00) | ((i<<8)&0x00ff0000) | (i<<24);
				// TOTALS: 4*shift + 2*and + 3*or + 1*assign
#endif
   return i;
}

static inline Uint16 littleendian_16(Uint16 s)
{
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
	return s>>8 | s<<8;
#endif
   return s;
}

/* -------------------- */
/**
   write_wav_header() was pasted from http://sfii90.googlecode.com/svn/trunk/src/audio/speex/wav_io.c
	wav_io.c is Copyright (C) 2002 Jean-Marc Valin.
	The following statements from wav_io.c apply to the write_wav_header method below:
		
		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions
		are met:
		
		- Redistributions of source code must retain the above copyright
		notice, this list of conditions and the following disclaimer.
		
		- Redistributions in binary form must reproduce the above copyright
		notice, this list of conditions and the following disclaimer in the
		documentation and/or other materials provided with the distribution.
		
		- Neither the name of the Xiph.org Foundation nor the names of its
		contributors may be used to endorse or promote products derived from
		this software without specific prior written permission.
		
		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
		``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
		LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
		A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR
		CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
		EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
		PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
		PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
		LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
		NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 **/
void write_wav_header(FILE *file, int rate, int channels, int format, int size)
{
   char ch[5];
   Uint32 itmp;
   Uint16 stmp;

   ch[4]=0;

   fprintf (file, "RIFF");

   itmp = littleendian_32( size - 8); 
   fwrite(&itmp, 4, 1, file);

   fprintf (file, "WAVEfmt ");

   itmp = littleendian_32((Uint32)16);
   fwrite(&itmp, 4, 1, file);

   stmp = littleendian_16((Uint16)1);
   fwrite(&stmp, 2, 1, file);

   stmp = littleendian_16(channels);
   fwrite(&stmp, 2, 1, file);

   itmp = littleendian_32(rate);
   fwrite(&itmp, 4, 1, file);

   itmp = littleendian_32(rate*channels*2);
   fwrite(&itmp, 4, 1, file);

   stmp = littleendian_16(2*channels);
   fwrite(&stmp, 2, 1, file);

   stmp = littleendian_16(16);
   fwrite(&stmp, 2, 1, file);

   fprintf (file, "data");

   itmp = littleendian_32( size - 44);
   fwrite(&itmp, 4, 1, file);
}

/* --------------------- */
static void game_SilenceDetected()				// on the leading edge of n seconds of silence
{
	int i;
	char fname[PATH_MAX];

	capture_Stop();
	playback_QueueSound( MEMO_CHAN, snd_effect_stop[TOOL_MEMO_RECORD]);
	
	// playback recorded audio
	snprintf ( fname, sizeof ( fname ),
		"%s/memos/%s",
		getSaveDir(), audio_out_filename);
	playback_QueueSound( MEMO_CHAN, Mix_LoadWAV(fname));
	playback_QueueSound( MEMO_CHAN, snd_effect_stop[TOOL_MEMO_RECORD]);
}

/* --------------------- */
static void game_StartCapture()				// capture to 
{
	SDL_AudioSpec spec, result;

	if( !audioin_initialised) {
		SDL_InitAudioIn();
		spec.format=AUDIO_FORMAT;
		spec.freq=AUDIO_SAMPLERATE;
		spec.callback=capture_ProcessAudioData;
		spec.samples=AUDIO_SAMPLES;
		spec.channels=AUDIO_CHANNELS;
			
		spec.userdata  = NULL;
		
		if(SDL_OpenAudioIn(&spec,&result)<0)
		{
			fprintf(stderr,"Couldn't open audio input device: %s\n",SDL_GetError());
			SDL_CloseAudioIn();
			return 5;
		}
		SDL_PauseAudioIn( 0);
		audioin_initialised = 1;
	}
	playback_QueueSound( MEMO_CHAN, snd_effect_start[TOOL_MEMO_RECORD]);
	// don't start recording until the playback_isplaying sounds are done
	while( playback_isplaying)
		SDL_Delay( 10);

	SDL_Delay(450);
	capture_Start();
}

/* --------------------- */
static void game_Stop()
{
	if( playback_isplaying)
		playback_Abort();
	if( recordto_filep) {
		capture_Stop();
		playback_QueueSound( MEMO_CHAN, snd_effect_stop[TOOL_MEMO_RECORD]);
		while( playback_isplaying)
			SDL_Delay( 10);
	}
}

/* --------------------- */
Uint32 memo_api_version ( void )
{
#ifdef DEBUG
	fprintf( stderr, "memo_api_version\n");
#endif
	return TP_MAGIC_API_VERSION;
}

int memo_init ( magic_api * api )
{
	int i;
	char fname[PATH_MAX];

#ifdef DEBUG
	fprintf( stderr, "memo_init\n");
#endif
	for ( i = 0; i < NUM_TOOLS; i++ )
	{
		snprintf ( fname, sizeof ( fname ),
		           "%s/sounds/magic/%s",
		           api->data_directory, tools[i].snd_filename_start );

#ifdef DEBUG
		fprintf(stderr, "Trying to load %s sound file\n", fname);
#endif
		snd_effect_start[i] = Mix_LoadWAV(fname);

		snprintf ( fname, sizeof ( fname ),
		           "%s/sounds/magic/%s",
		           api->data_directory, tools[i].snd_filename_stop );

		snd_effect_stop[i] = Mix_LoadWAV(fname);
	}

	return 1;
}


/* --------------------- */
int memo_get_tool_count ( magic_api * api )
{
#ifdef DEBUG
	fprintf( stderr, "memo_get_tool_count\n");
#endif
	return NUM_TOOLS;
}


/* --------------------- */
SDL_Surface * memo_get_icon ( magic_api * api, int which )
{
	SDL_Surface *image;
	char fname[PATH_MAX];

#ifdef DEBUG
	fprintf( stderr, "memo_get_icon\n");
#endif

	snprintf ( fname, sizeof ( fname ), "%s/images/magic/%s",
	           api->data_directory, tools[which].icon_filename );
#ifdef DEBUG
	fprintf( stderr, "DEBUG: Loading image %s\n", fname);
#endif

	image = IMG_Load ( fname );
	if( !image) {
		fprintf( stderr, "IMG_Load( %s): %s\n", fname, IMG_GetError());
	}
	return image;
}


/* --------------------- */
char * memo_get_name ( magic_api * api, int which )
{
	const char * our_name_english;
	const char * our_name_localized;

#ifdef DEBUG
	fprintf( stderr, "memo_get_name\n");
#endif
	our_name_english = tools[which].name;
	our_name_localized = gettext ( our_name_english );
	return strdup ( our_name_localized );
}

/* --------------------- */
char * memo_get_description ( magic_api * api, int which, int mode )
{
	const char * our_desc_english;
	const char * our_desc_localized;

#ifdef DEBUG
	fprintf( stderr, "memo_get_description\n");
#endif
	our_desc_english = tools[which].desc;
	our_desc_localized = gettext ( our_desc_english );
	return strdup ( our_desc_localized );
}

/* --------------------- */
int memo_requires_colors ( magic_api * api, int which )
{
	return 0;	// FALSE
}

/* --------------------- */
int memo_modes ( magic_api * api, int which )
{
	return MODE_FULLSCREEN;
}


// Shut down
//
// explain how SDL_audioin could be patched to make Open/CloseAudioIn work
// symmetrically for the DEVDSP (Linux) driver

/* --------------------- */
void memo_shutdown ( magic_api * api )
{
	game_Stop();
	SDL_CloseAudioIn();
}


/* --------------------- */
void memo_click ( magic_api * api, int which, int mode,
                     SDL_Surface * canvas, SDL_Surface * snapshot,
                     int x, int y, SDL_Rect * update_rect )
{
#ifdef DEBUG
	fprintf( stderr, "memo_click\n");
#endif
}


/* --------------------- */
void memo_drag ( magic_api * api, int which, SDL_Surface * canvas,
                    SDL_Surface * snapshot, int ox, int oy, int x, int y,
                    SDL_Rect * update_rect )
{
#ifdef DEBUG
	fprintf( stderr, "memo_drag\n");
#endif
}


/* --------------------- */
void memo_release ( magic_api * api, int which,
                       SDL_Surface * canvas, SDL_Surface * snapshot,
                       int x, int y, SDL_Rect * update_rect )
{
#ifdef DEBUG
	fprintf( stderr, "memo_release\n");
#endif
}


/* --------------------- */
void memo_set_color ( magic_api * api, Uint8 r, Uint8 g, Uint8 b )
{
	// this tool uses no color
}

/* --------------------- */
void memo_switchin ( magic_api * api, int which, int mode, SDL_Surface * canvas )
{
	int i;	// common iterator
#ifdef DEBUG
	fprintf( stderr, "memo_switchin\n");
#endif
	game_StartCapture();
	return;
}

/* --------------------- */
void memo_switchout ( magic_api * api, int which, int mode, SDL_Surface * canvas )
{
#ifdef DEBUG
	fprintf( stderr, "memo_switchout\n");
#endif
	game_Stop();
	return;
}

// this method was pasted from tuxpaint.c
/* --------------------- */
static char * getSaveDir()
{
  char *savedir = NULL;

#ifdef WIN32
  /* Windows */

  savedir = GetDefaultSaveDir("TuxPaint");
#elif __BEOS__
  /* BeOS */

  savedir = strdup("./userdata");
#elif __APPLE__
  /* Mac OS X */
  
  savedir = strdup(macosx.preferencesPath);
#else
  /* Linux */

  if (getenv("HOME") != NULL)
  {
    char tmp[PATH_MAX];

    snprintf(tmp, PATH_MAX, "%s/%s", getenv("HOME"), ".tuxpaint");

    savedir = strdup(tmp);
  }
  else
  {
    /* Woah, don't know where $HOME is? */

    fprintf(stderr, "Error: You have no $HOME environment variable!\n");
    exit(1);
  }
#endif

  return savedir;
}
